<template>
	<view class="media-box">
		<view class="media-item" v-for='(item, index) in sources' :key="index"
			:style="{width: itemSize.width, height: itemSize.height}">
			<media-upload-item :ref="'item_'+index" :itemSize='itemSize' :mediaData='item'
				@tapImage='tapImage($event, index)' @tapVideo='tapVideo($event, index)'>
				<template v-slot:topLeft>
					<view class="corner-button delete-button" @click.stop="tapDelete(item, index)">
						<uni-icons type="clear" color="#ff2525" size="26"></uni-icons>
					</view>
				</template>
				<template v-slot:cover>
					<view class="cover-bg">
						<view class="cover-content">
							<cu-circle
								v-if='mediaUploadStatusList[index].status == uploadStatus.WAITING.value || mediaUploadStatusList[index].status == uploadStatus.UPLOADING.value'
								:cid="'downloading-'+index" :size='50' :transparent="true" strokeColor="#888"
								progressColor="#fff" :strokeWidth="6" :percent='mediaUploadStatusList[index].progress'>
								<view class="progress">
									{{mediaUploadStatusList[index].progress}}%
								</view>
							</cu-circle>
							<view class="status-box"
								v-else-if='mediaUploadStatusList[index].status == uploadStatus.SUCCESS.value'>
								<uni-icons type='checkbox' color='#5eb670' size='20'></uni-icons>
								<view class="status success">
									上传成功
								</view>
							</view>
							<view class="status-box"
								v-else-if='mediaUploadStatusList[index].status == uploadStatus.FAIL.value'
								@click.stop="tapRetry(index)" style="pointer-events: auto;">
								<uni-icons type='refresh' color='#ff2525' size='20'></uni-icons>
								<view class="status fail">
									{{mediaUploadStatusList[index].errMsg ? mediaUploadStatusList[index].errMsg : "失败重试"}}
								</view>
							</view>
						</view>
					</view>
				</template>
			</media-upload-item>
		</view>
		<view class="media-item" :style="{width: itemSize.width, height: itemSize.height}" v-if='showAddButton'>
			<view class="add-button" @click="tapAddMedia">
				<uni-icons type="plusempty" color="#888" size="36"></uni-icons>
			</view>
		</view>
	</view>
</template>

<script>
	import {
		analyticsMediaType,
		isEmpty,
		isNotEmpty,
		logInfo,
		px2rpx
	} from '../../utils/index.js'
	import {
		ENUM_MEDIA_TYPE,
		ENUM_MEDIA_UPLOAD_STATUS
	} from "../../static/enum.js"
	import {
		uploadRequest
	} from "../../utils/request/index.js"
	import {
		UploadParam
	} from '../../utils/request/requestModels.js';
	import ComponentMixin from "../../mixins/common-component-mixin.js";
	import mediaUploadItem from '../cu-media-item/cu-media-item.vue';
	import cuCircle from "../cu-circle/cu-circle.vue";
	import MediaModel from '../cu-media-item/media-model.js';
	import {
		compressMedia
	} from '../../utils/tools/index.js';
	class MediaUploadStatus {
		constructor() {
			this.isUpoading = false;
			this.status = ENUM_MEDIA_UPLOAD_STATUS.WAITING.value;
			this.progress = 0;
			this.result = null;
			this.resultType = null;
			this.errMsg = null;
		}
	}
	export default {
		mixins: [ComponentMixin],
		components: {
			mediaUploadItem,
			cuCircle
		},
		props: {
			// 初始列表
			initList: {
				type: Array,
				default: () => {
					return []
				},
				validator: function(value) {
					return value.every(item => {
						return item instanceof MediaModel || typeof item == 'string'
					})
				},
			},
			// 最大上传数量
			// 微信小程序最大上传数量不能超过9
			maxCount: {
				type: Number,
				default: 9,
				validator: function(value) {
					return typeof value === 'number' && value >= 0 && value <= 9
				},
				require: false
			},
			// 列数
			colCount: {
				type: Number,
				default: 3,
				validator: function(value) {
					return typeof value === 'number' && value >= 1 && value <= 5
				},
				require: false
			},
			// 宽高比(高度相对于宽度的比值)
			aspectRatio: {
				type: Number,
				default: 1.2,
				validator: function(value) {
					return typeof value == 'number' && value > 0
				},
				require: false
			},
			// 文件类型
			mediaType: {
				type: Array,
				default: function() {
					return ['mix']
				},
				validator: function(value) {
					return value.every(item => {
						return ['image', 'video', 'mix'].includes(item)
					})
				},
				require: false
			},
			// 来源
			sourceType: {
				type: Array,
				default: function() {
					return ['album', 'camera']
				},
				validator: function(value) {
					return value.every(item => {
						return ['album', 'camera'].includes(item)
					})
				},
				require: false
			},
			// 视频最大时长
			maxDuration: {
				type: Number,
				default: 20,
				validator: function(value) {
					return typeof value === 'number' && value >= 0 && value <= 60
				},
				require: false
			},
			// 文件尺寸类型（仅图片支持）
			sizeType: {
				type: Array,
				default: function() {
					return ['compressed', 'original']
				},
				validator: function(value) {
					return value.every(item => {
						return ['compressed', 'original'].includes(item)
					})
				},
				require: false
			},
			// 前置/后置摄像头（仅视频支持）
			camera: {
				type: String,
				default: 'back',
				validator: function(value) {
					return ['back', 'front'].includes(value)
				},
				require: false
			},
			// 是否开启自动上传，即选择完文件后使用默认方法立即上传，如果为否，需要外部主动调用startUpload方法开启上传。
			autoUpload: {
				type: Boolean,
				default: true,
			},
			// 默认上传方法参数
			defaultUploadParam: {
				type: Object,
				default: function() {
					return {
						useBaseUrl: true,
						url: "/travel/file/upload",
						name: 'file',
						header: {},
						formData: {},
						timeout: 6000
					}
				}
			},
			// 压缩最大尺寸 KB, 小于等于0时不压缩。
			// 选择图片时，如果图片尺寸大于该值，则会压缩图片。
			// 选择视频时，如果视频尺寸大于该值，则会压缩视频。
			compressMaxSize: {
				type: Number,
				default: 0
			},
			// 达到最大数量时是否隐藏新增按钮
			hideAddButtonWhenFull: {
				type: Boolean,
				default: true
			},
			// 是否自动计算item尺寸
			autoCalcItemSize: {
				type: Boolean,
				default: true, // false 时  需要在恰当的时候手动调用 resetItemSize方法
			}
		},
		data() {
			return {
				itemSize: {
					width: '',
					height: ''
				},
				sources: [], // 上传源文件列表
				mediaUploadStatusList: [], // 上传结果列表
				uploadStatus: ENUM_MEDIA_UPLOAD_STATUS,
				customUploadFnList: [], // 自定义上传方法列表
			}
		},
		watch: {
			initList: {
				handler: function(newVal) {
					if (isNotEmpty(newVal) && isEmpty(this.sources)) {
						if (!(this.sources instanceof Array)) {
							this.sources = []
						}
						this.sources = newVal.map(item => {
							let mediaUploadStatus = new MediaUploadStatus();
							let source;
							if (item instanceof MediaModel) {
								source = item;
							} else if (typeof item == 'string') {
								let imageType = analyticsMediaType(item);
								if (imageType.value == ENUM_MEDIA_TYPE.IMAGE.value || imageType.value ==
									ENUM_MEDIA_TYPE.VIDEO.value) {
									source = new MediaModel(item, imageType.value);
								} else {
									source = null;
								}
							}
							if (source) {
								mediaUploadStatus.isUpoading = false;
								mediaUploadStatus.status = ENUM_MEDIA_UPLOAD_STATUS.SUCCESS.value;
								mediaUploadStatus.progress = 0;
								mediaUploadStatus.result = source.imageUrl;
								mediaUploadStatus.resultType = source.imageType;
								this.mediaUploadStatusList.push(mediaUploadStatus);
							}
							return source;
						}).filter(item => {
							return item;
						})
					}
				},
				immediate: true
			},
			colCount: {
				handler: function(newVal) {
					if (this.autoCalcItemSize) {
						this.resetItemSize(newVal);
					}
				},
				immediate: true
			},
		},
		computed: {
			showAddButton: function() {
				return !this.hideAddButtonWhenFull || this.sources.length < this.maxCount;
			},
			// 是否需要压缩
			needCompressed: function() {
				return this.compressMaxSize > 0
			},
			// 是否上传中
			isUploading: function() {
				return this.mediaUploadStatusList.some(item => {
					return item.status == ENUM_MEDIA_UPLOAD_STATUS.UPLOADING.value
				})
			},
			// 是否超出最大上传数量
			overSizeAbleCount: function() {
				return this.maxCount <= this.sources.length;
			}
		},
		methods: {
			/**
			 * 开始上传
			 * @param {Number} index 上传索引
			 * @param {Array} uploadFnList 上传方法列表，执行sources在index位置上的上传时，会使用uploadFnList在index位置上的方法上传，如果不能找到对应的方法，则会使用默认上传方法执行上传，uploadFnList中的方法入参和返回值，请参照defaultUploadFn正确设置，否则会存在上传失败，或者进度条显示不正确的问题。
			 */
			startUpload(index = 0, uploadFnList = []) {
				this.customUploadFnList = uploadFnList || [];
				return this.triggerUpload(index);
			},
			// 重置item尺寸
			resetItemSize(col) {
				const query = uni.createSelectorQuery().in(this);
				query.select('.media-box').boundingClientRect();
				query.exec((res) => {
					if (res[0]) {
						const width = res[0].width;
						if (width) {
							let calcWidth = px2rpx(width / (col ? col : this.colCount))
							this.itemSize.width = `${calcWidth-10}rpx`;
							this.itemSize.height = `${calcWidth * this.aspectRatio -10}rpx`;
						}
					}
				})
			},
			// 点击图片
			tapImage(url, index) {
				this.$refs['item_' + index][0].previewImage(this.sources.filter(item => {
					return item.imageType == ENUM_MEDIA_TYPE.IMAGE.value
				}).map(item => {
					return item.imageUrl
				}), url)
			},
			// 点击视频
			tapVideo(url, index) {
				this.$refs['item_' + index][0].previewVideo(url)
			},
			// 点击删除
			tapDelete(media, index) {
				// 判断上传状态，上传中不允许执行删除，因为上传是递归并异步执行，可能会导致错误
				if (this.isUploading) {
					uni.showToast({
						title: "正在上传中，请稍后操作",
						icon: 'none'
					})
					return;
				};
				// 删除源文件
				this.sources.splice(index, 1);
				// 删除上传状态
				this.mediaUploadStatusList.splice(index, 1);
				this.triggerDelete(media, index);
			},
			// 点击重试
			tapRetry(index) {
				this.$set(this.mediaUploadStatusList[index], "errMsg", null);
				this.$set(this.mediaUploadStatusList[index], 'status', ENUM_MEDIA_UPLOAD_STATUS.WAITING.value)
				this.triggerStart();
				this.triggerUpload(index);
			},
			// 点击新增按钮
			tapAddMedia() {
				// 判断上传状态，上传中不允许执行选择，因为上传是递归并异步执行，可能会导致错误
				if (this.isUploading) {
					uni.showToast({
						title: "正在上传中，请稍后操作",
						icon: 'none'
					})
					return;
				};
				// 判断是否超出上传数量
				let count = this.maxCount - this.sources.length;
				if (count <= 0) {
					uni.showToast({
						title: "超出最大上传限制！",
						icon: 'none'
					})
					return;
				}
				const {
					mediaType,
					sourceType,
					maxDuration,
					sizeType,
					camera
				} = this;
				uni.chooseMedia({
					count,
					mediaType,
					sourceType,
					maxDuration,
					sizeType,
					camera,
					success: res => {
						const {
							tempFiles,
							type
						} = res;
						// 遍历选中的文件，拼装sources列表及mediaUploadStatusList列表
						let chooseList = [];
						tempFiles.forEach(item => {
							const {
								tempFilePath,
								size,
								duration,
								height,
								width,
								thumbTempFilePath,
								fileType
							} = item;
							let imageType = fileType == 'image' ? ENUM_MEDIA_TYPE.IMAGE.value :
								fileType == 'video' ? ENUM_MEDIA_TYPE.VIDEO.value : ENUM_MEDIA_TYPE
								.UNKNOW.value
							let mediaSource = new MediaModel(tempFilePath, imageType,
								thumbTempFilePath);
							chooseList.push(mediaSource);
							this.sources.push(mediaSource);
							let mediaUploadStatus = new MediaUploadStatus();
							mediaUploadStatus.resultType = mediaSource.imageType;
							this.mediaUploadStatusList.push(mediaUploadStatus);
						})
						// 通知外部选中的sources
						this.triggerChoose(chooseList);
						// 是否开启自动上传
						if (this.autoUpload) {
							// 执行自动上传
							this.startUpload();
						}
					},
					fail: err => {
						uni.showToast({
							title: "选择文件失败！",
							icon: 'none'
						})
					}
				})
			},

			/**
			 * 默认上传方法
			 * @param {Object} imageUrl // 文件路径
			 * @param {function(progress)} progressUpdateCallback // 进度回调
			 */
			defaultUploadFn(imageUrl, progressUpdateCallback) {
				return uploadRequest(new UploadParam({
					...this.defaultUploadParam,
					filePath: imageUrl,
					signal: {
						onProgressUpdate: (res) => {
							const {
								progress,
								totalBytesSent,
								totalBytesExpectedToSend
							} = res;
							typeof progressUpdateCallback == 'function' && progressUpdateCallback({
								progress,
								totalBytesSent,
								totalBytesExpectedToSend
							});
						}
					}
				}))
			},
			/**
			 * 执行上传
			 * @param {Number} index 上传索引
			 */
			triggerUpload(index = 0) {
				// index 超出 sources长度，表示上传完成
				if (index >= this.sources.length) {
					this.triggerFinish();
					return Promise.resolve();
				}
				// index == 0, 标识开始上传
				if (index == 0) {
					this.triggerStart()
				}
				// 获取要上传的对象
				let uploadItem = this.sources[index];
				// 获取上传对象对应的上传状态
				let uploadItemStatus = this.mediaUploadStatusList[index];
				if (uploadItemStatus.status == ENUM_MEDIA_UPLOAD_STATUS.SUCCESS.value || uploadItemStatus.status ==
					ENUM_MEDIA_UPLOAD_STATUS.FAIL.value || uploadItemStatus.status == ENUM_MEDIA_UPLOAD_STATUS
					.UPLOADING
					.value) {
					// 如果上传状态为成功|失败|上传中，则跳过该对象的上传
					// 执行下一个对象的上传
					return this.triggerUpload(index + 1);
				}
				// 上传动作
				let uploadAction = (uploadItem) => {
					// 获取上传方法定义
					let uploadFn;
					if (index < this.customUploadFnList.length && typeof this.customUploadFnList[index] ==
						'function') {
						// 如果index 有对应的自定义上传方法，则使用自定义上传方法
						uploadFn = this.customUploadFnList[index];
					} else {
						// 如果没有自定义上传方法，则使用默认上传方法
						uploadFn = this.defaultUploadFn;
					}
					// 如果上传状态为等待上传，则执行上传
					// 修改上传状态为上传中
					this.$set(this.mediaUploadStatusList[index], "errMsg", null);
					this.$set(this.mediaUploadStatusList[index], "status", ENUM_MEDIA_UPLOAD_STATUS.UPLOADING
						.value);
					// 执行上传
					return uploadFn.call(this, uploadItem.imageUrl, (progressRes) => {
						// 根据进度回调，更新上传进度
						const {
							progress,
							totalBytesSent,
							totalBytesExpectedToSend
						} = progressRes
						logInfo('progressUpdate', progress, totalBytesSent, totalBytesExpectedToSend);
						this.$set(this.mediaUploadStatusList[index], "progress", progress)
					}).then(res => {
						// 上传成功
						logInfo(res)
						// 修改上传状态为成功
						this.$set(this.mediaUploadStatusList[index], "errMsg", null);
						this.$set(this.mediaUploadStatusList[index], "status", ENUM_MEDIA_UPLOAD_STATUS
							.SUCCESS
							.value);
						// 修改上传状态的上传结果
						this.$set(this.mediaUploadStatusList[index], "result", res.imageUrl);
						// 执行下一个对象的上传
						return this.triggerUpload(index + 1);
					}).catch(err => {
						// 上传失败
						logInfo(err)
						// 修改上传状态为失败
						this.$set(this.mediaUploadStatusList[index], "errMsg", err);
						this.$set(this.mediaUploadStatusList[index], "status", ENUM_MEDIA_UPLOAD_STATUS
							.FAIL
							.value);
						this.$set(this.mediaUploadStatusList[index], "progress", 0);
						return this.triggerUpload(index + 1);
					})
				}
				// 是否需要压缩过大的文件
				if (this.needCompressed) {
					// 执行压缩并更新文件路径后执行上传
					let promise = compressMedia(uploadItem.imageUrl, uploadItem.imageType, this.compressMaxSize)
					return promise.then(compressFilePath => {
							// 压缩完成，将压缩后的临时文件地址赋值给当前上传对象
							this.$set(this.sources[index], "imageUrl", compressFilePath);
							uploadItem = this.sources[index];
						})
						.then(() => {
							// 执行上传
							return uploadAction(uploadItem);
						})
						.catch(err => {
							// 压缩失败，无法上传，修改状态
							// 上传失败
							logInfo(err)
							// 修改上传状态为失败
							this.$set(this.mediaUploadStatusList[index], "errMsg", err);
							this.$set(this.mediaUploadStatusList[index], "status", ENUM_MEDIA_UPLOAD_STATUS
								.FAIL
								.value);
							this.$set(this.mediaUploadStatusList[index], "progress", 0);
							return this.triggerUpload(index + 1);
						})
				} else {
					// 不需要，直接执行上传
					return uploadAction(uploadItem);
				}

			},
			// 通知选中媒体文件
			// chooseList: 新选中的文件
			// chooseResultList: 所有选中的文件
			triggerChoose(chooseList, chooseResultList = this.sources) {
				this.$emit('choose', {
					chooseList,
					chooseResultList
				})
			},
			// 通知开始上传
			triggerStart() {
				this.$emit('start')
			},
			// 通知结束上传
			triggerFinish() {
				let uploadResult = this.packageUploadResult();
				this.$emit('finish', uploadResult)
			},
			// 通知删除
			triggerDelete(deleteItem, deleteIndex) {
				let deleteResult = this.packageUploadResult();
				this.$emit('delete', {
					deleteItem,
					deleteIndex,
					deleteResult
				})
			},
			// 获取上传结果
			packageUploadResult() {
				return this.mediaUploadStatusList.filter(item => {
					return item.status == ENUM_MEDIA_UPLOAD_STATUS.SUCCESS.value
				}).map(item => {
					return {
						imageUrl: item.result,
						imageType: item.resultType
					}
				})
			}
		}
	}
</script>

<style lang="scss" scoped>
	.media-box {
		display: flex;
		flex-direction: row;
		flex-wrap: wrap;
		margin-left: 10rpx;

		.media-item {
			display: flex;
			flex-direction: column;
			margin-right: 10rpx;
			margin-bottom: 10rpx;
			border-radius: 14rpx;
			overflow: hidden;

			&::last-child {
				margin-right: 0;
			}

			.add-button {
				display: flex;
				justify-content: center;
				align-items: center;
				width: calc(100% - 8rpx);
				height: calc(100% - 8rpx);
				margin: 4rpx;
				background-color: #f5f5f5;
				border: 1rpx dashed #888;
				border-radius: 14rpx;
				overflow: hidden;
			}

			.corner-button {
				width: 100%;
				height: 100%;
				display: flex;
				flex-direction: column;
				background: #ffffff44;
			}

			.delete-button {
				align-items: flex-start;
				justify-content: flex-start;
				border-bottom-right-radius: 50%;
			}

			.download-button {
				align-items: flex-end;
				justify-content: flex-start;
				border-bottom-left-radius: 50%;
			}

			.cover-bg {
				width: 100%;
				height: 100%;
				background: #88888844;
				display: flex;
				flex-direction: column;
				align-items: center;
				justify-content: center;
				position: absolute;
				z-index: 1;

				.cover-content {
					width: 100%;
					height: 100%;
					display: flex;
					flex-direction: column;
					align-items: center;
					justify-content: center;

					.status-box {
						display: flex;
						flex-direction: column;
						align-items: center;
						justify-content: center;

						.status {
							font-size: 22rpx;
							text-align: center;
							word-break: break-all;
							overflow: hidden;
							display: -webkit-box;
							-webkit-line-clamp: 2;
							-webkit-box-orient: vertical;

							&.success {
								color: #5eb670;
							}

							&.fail {
								color: #ff2525;
							}
						}
					}
				}
			}
		}
	}
</style>